package edu.udo.sopra10.chaturaji.controllers;

import static edu.udo.sopra10.chaturaji.controllers.SystemController.getMailPassword;
import static edu.udo.sopra10.chaturaji.controllers.SystemController.getMailServer;
import static edu.udo.sopra10.chaturaji.controllers.SystemController.getMailUsername;
import static edu.udo.sopra10.chaturaji.controllers.SystemController.getSenderMailAddress;
import static edu.udo.sopra10.chaturaji.controllers.SystemController.recordHighscore;
import static edu.udo.sopra10.chaturaji.controllers.SystemController.serializeTurn;
import static edu.udo.sopra10.chaturaji.controllers.SystemController.spawnThread;
import static edu.udo.sopra10.chaturaji.entities.Player.Type.EMAIL;
import static edu.udo.sopra10.chaturaji.entities.Player.Type.HUMAN;
import static edu.udo.sopra10.chaturaji.entities.transfer.DiceRoll.equivalenceClassFromRoll;
import static edu.udo.sopra10.chaturaji.util.CollectionUtil.addToListMap;
import static edu.udo.sopra10.chaturaji.util.ExceptionUtil.assertNotNull;
import static java.util.Arrays.asList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Map.Entry;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.SimpleEmail;

import edu.udo.sopra10.chaturaji.ai.HintGivers;
import edu.udo.sopra10.chaturaji.entities.Game;
import edu.udo.sopra10.chaturaji.entities.Move;
import edu.udo.sopra10.chaturaji.entities.Pass;
import edu.udo.sopra10.chaturaji.entities.PieceMove;
import edu.udo.sopra10.chaturaji.entities.Player;
import edu.udo.sopra10.chaturaji.entities.Square;
import edu.udo.sopra10.chaturaji.entities.Turn;
import edu.udo.sopra10.chaturaji.entities.coordinates.Movement;
import edu.udo.sopra10.chaturaji.entities.coordinates.Position;
import edu.udo.sopra10.chaturaji.entities.pieces.Boat;
import edu.udo.sopra10.chaturaji.entities.pieces.Elephant;
import edu.udo.sopra10.chaturaji.entities.pieces.Horse;
import edu.udo.sopra10.chaturaji.entities.pieces.King;
import edu.udo.sopra10.chaturaji.entities.pieces.Pawn;
import edu.udo.sopra10.chaturaji.entities.pieces.Piece;
import edu.udo.sopra10.chaturaji.entities.pieces.Piece.Actions;
import edu.udo.sopra10.chaturaji.entities.transfer.DiceRoll;
import edu.udo.sopra10.chaturaji.entities.transfer.MoveRequest;
import edu.udo.sopra10.chaturaji.entities.transfer.DiceRoll.EquivalenceClass;
import edu.udo.sopra10.chaturaji.events.BoardResetEvent;
import edu.udo.sopra10.chaturaji.events.BoardResetEventListener;
import edu.udo.sopra10.chaturaji.events.EventListenerList;
import edu.udo.sopra10.chaturaji.events.GameOverEvent;
import edu.udo.sopra10.chaturaji.events.GameOverEventListener;
import edu.udo.sopra10.chaturaji.events.MoveEvent;
import edu.udo.sopra10.chaturaji.events.MoveEventListener;
import edu.udo.sopra10.chaturaji.events.MoveRequestEvent;
import edu.udo.sopra10.chaturaji.events.MoveRequestEventListener;
import edu.udo.sopra10.chaturaji.events.PassEvent;
import edu.udo.sopra10.chaturaji.events.PassEventListener;
import edu.udo.sopra10.chaturaji.events.TurnRequestEvent;
import edu.udo.sopra10.chaturaji.events.TurnRequestEventListener;
import edu.udo.sopra10.chaturaji.exceptions.BadStateException;
import edu.udo.sopra10.chaturaji.exceptions.IllegalMoveException;
import edu.udo.sopra10.chaturaji.exceptions.NoPieceException;
import edu.udo.sopra10.chaturaji.exceptions.NoPieceOwnershipException;
import edu.udo.sopra10.chaturaji.util.CloneUtil;

/**
 * The controller singleton for running games. This implements every operation
 * available for game modification, but no meta-game features such as saving and
 * loading.
 * 
 * @author Simon Dierl
 */
public final class GameController {
	/**
	 * The state the controller is in.
	 * 
	 * @author Simon Dierl
	 */
	private static enum Phase {
		/**
		 * Wait for a {@link GameController#move(Position, Position)} that fits
		 * the stored first dice roll or a {@link GameController#pass()} to make
		 * the first move. Next state is {@link #MOVE2}.
		 */
		MOVE1,
		/**
		 * Wait for a {@link GameController#move(Position, Position)} that fits
		 * the stored second dice roll or a {@link GameController#pass()} to
		 * make the second move. After this state, the next player gets her
		 * turn; next state is {@link #ROLL}.
		 */
		MOVE2,
		/**
		 * Replay the game. Only {@link GameController#undo()} and
		 * {@link GameController#redo()} are available, but skip back or forward
		 * one turn at a time. This state may not be left.
		 */
		REPLAY,
		/**
		 * Wait for a {@link GameController#roll()} (for local and AI players)
		 * or a {@link GameController#enterRoll(byte, byte)} (for email players)
		 * to acquire the dice results for this turn. Next state is
		 * {@link #MOVE1}.
		 */
		ROLL;
	}

	/**
	 * The score awarded for assassinating all kings.
	 */
	private static final byte ALL_KING_ASSASSINATION_SCORE = 54;

	/**
	 * The board reset event listeners.
	 */
	private static EventListenerList<BoardResetEventListener> boardResetEventListeners = new EventListenerList<BoardResetEventListener>();

	/**
	 * The player whose turn it is.
	 */
	private static Player currentPlayer;

	/**
	 * The turn that is currently being created.
	 */
	private static Turn currentTurn;

	/**
	 * The game we are controlling.
	 */
	private static Game game;

	/**
	 * Secures write access to the game to avoid concurrent modification.
	 */
	private static Lock gameLock = new ReentrantLock();
	
	/**
	 * The game over request event listeners.
	 */
	private static EventListenerList<GameOverEventListener> gameOverEventListeners = new EventListenerList<GameOverEventListener>();
	
	/**
	 * Secures synchronized execution of events.
	 */
	private static Lock eventLock = new ReentrantLock();

	/**
	 * The move event listeners.
	 */
	private static EventListenerList<MoveEventListener> moveEventListeners = new EventListenerList<MoveEventListener>();

	/**
	 * The move request event listeners.
	 */
	private static EventListenerList<MoveRequestEventListener> moveRequestEventListeners = new EventListenerList<MoveRequestEventListener>();

	/**
	 * The move request event listeners.
	 */
	private static EventListenerList<PassEventListener> passEventListeners = new EventListenerList<PassEventListener>();

	/**
	 * The phase we are in right now.
	 */
	private static Phase phase;

	/**
	 * The source of randomness.
	 */
	private static Random random = new Random();

	/**
	 * If event firing shall be disabled.
	 */
	private static boolean suppressEvents;

	/**
	 * The position to truncate the history from, {@code null} if we are not
	 * replaying.
	 */
	private static Integer truncate;

	/**
	 * The turn request event listeners.
	 */
	private static EventListenerList<TurnRequestEventListener> turnRequestEventListeners = new EventListenerList<TurnRequestEventListener>();

	/**
	 * The AI object that is able to give hints.
	 */
	private static HintGivers hintGiver;

	/**
	 * Returns the hint giver.
	 * 
	 * @return the hintGiver.
	 */
	public static HintGivers getHintGiver ()
	{
		return hintGiver;
	}

	/**
	 * Abort the current game. This means the current player can no longer
	 * score.
	 */
	public static void abortGame() {
		currentPlayer.taint();
		recordHighscore(game);
		processEvent(new GameOverEvent(GameController.class, true));
	}

	/**
	 * Adds the BoardResetEvent listener.
	 * 
	 * @param eventListener
	 *            event listener object.
	 */
	public static void addBoardResetEventListener(
			final BoardResetEventListener eventListener) {
		boardResetEventListeners.addListener(eventListener);
	}
	
	/**
	 * Adds the GameOverEvent listener.
	 * 
	 * @param eventListener
	 *            event listener object.
	 */
	public static void addGameOverEventListener(
			final GameOverEventListener eventListener) {
		gameOverEventListeners.addListener(eventListener);
	}

	/**
	 * Adds the MoveEvent listener.
	 * 
	 * @param eventListener
	 *            event listener object.
	 */
	public static void addMoveEventListener(
			final MoveEventListener eventListener) {
		moveEventListeners.addListener(eventListener);
	}

	/**
	 * Adds the MoveRequestEvent listener.
	 * 
	 * @param eventListener
	 *            event listener object.
	 */
	public static void addMoveRequestEventListener(
			final MoveRequestEventListener eventListener) {
		moveRequestEventListeners.addListener(eventListener);
	}

	/**
	 * Adds the PassEvent listener.
	 * 
	 * @param eventListener
	 *            event listener object.
	 */
	public static void addPassEventListener(
			final PassEventListener eventListener) {
		passEventListeners.addListener(eventListener);
	}

	/**
	 * Adds the TurnRequestEvent listener.
	 * 
	 * @param eventListener
	 *            event listener object.
	 */
	public static void addTurnRequestEventListener(
			final TurnRequestEventListener eventListener) {
		turnRequestEventListeners.addListener(eventListener);
	}

	/**
	 * Manually enter a roll. This is used by email players ONLY.
	 * 
	 * @param firstRoll
	 *            the result of the first die.
	 * @param secondRoll
	 *            the result of the second die.
	 */
	public static void enterRoll(final byte firstRoll, final byte secondRoll) {
		synchronized (gameLock) {
			requireRollState();
			currentTurn.addUnassignedRolls(asList(firstRoll, secondRoll));
		}

		nextPhase();
	}

	/**
	 * Get the game.
	 * 
	 * @return the game, never {@code null}.
	 */
	public static Game getGame() {
		return game;
	}

	/**
	 * Get all {@link Actions} a piece can perform.
	 * 
	 * @param start
	 *            the position of the piece. Must be occupied and owned by the
	 *            current player.
	 * @return the possible moves.
	 */
	public static Actions getPossibleActions(final Position start) {
		assertNotNull(start, "start position");
		final Piece piece = game.getBoard()[start.getX()][start.getY()]
				.getPiece();
		checkPieceNotNullAndOwned(piece);
		return piece.getPossibleActions();
	}

	/**
	 * Returns true if there is a piece owned by the current player at the given
	 * position on the board.
	 * 
	 * @param pos
	 *            position to check.
	 * @return true if the position has such a piece.
	 */
	public static boolean hasOwnedPiece(final Position pos) {
		if (pos == null) {
			return false;
		}

		final Piece piece = game.getBoard()[pos.getX()][pos.getY()].getPiece();
		return ((piece != null) && piece.getOwner().equals(currentPlayer));
	}

	/**
	 * Sets the AI that gives hints.
	 * 
	 * @param ai the AI object.
	 */
	public static void setHintGiver ( HintGivers ai )
	{
		hintGiver = ai;
	}
	
	/**
	 * Gets a hint for the given player using the HintGiver AI.
	 * 
	 * @param rolls unassigned rolls.
	 * @return the suggested MoveRequest.
	 */
	public static MoveRequest getHint ( List<Byte> rolls )
	{
		currentPlayer.taint();
		
		if ( hintGiver == null )
			return null;
		
		return hintGiver.getHint( rolls, currentPlayer.getTurnOrderPosition() );
	}

	/**
	 * Jump as close to the specified turn in history as possible. In a normal
	 * game (1 player, 3 AIs), this uses a special counting scheme: four turns
	 * (by four players) are counted as one turn. If the game is a replay, one
	 * turn corresponds to one turn. If the turn number is out of bounds, the
	 * end or beginning of the history is navigated to.
	 * 
	 * @param turn
	 *            the turn to jumped to.
	 * @return the turn that was jumped to.
	 */
	public static int jumpHistory(final int turn) {
		requireUndoAllowed();
		final int maxTurn = (game.getHistory().size() + 3) / 4;
		int realTurn;
		if (turn > maxTurn) {
			realTurn = maxTurn;
		} else if (turn < 0) {
			realTurn = 0;
		} else {
			realTurn = turn;
		}

		int thisTurn;
		if (truncate != null) {
			thisTurn = truncate + 3 / 4;
		} else {
			thisTurn = maxTurn;
		}

		if (realTurn < thisTurn) {
			if (phase != Phase.ROLL) {
				undoInternal();
			}
			for (; thisTurn > realTurn; thisTurn--) {
				undoInternal();
			}
		} else if (realTurn > thisTurn) {
			for (; thisTurn < realTurn; thisTurn++) {
				redoInternal();
			}
		}

		return realTurn;
	}

	/**
	 * Attempt to move a piece.
	 * 
	 * @param start
	 *            the position where the move begins. The position must contain
	 *            a {@link Piece} and the piece must be owned by the current
	 *            player. Must not be {@code null}.
	 * @param end
	 *            the position where the move ends. The movement must constitute
	 *            a legal move. Must not be {@code null}.
	 */
	public static void move(final Position start, final Position end) {
		assertNotNull(start, "start position");
		assertNotNull(end, "end position");
		
		PieceMove move;

		synchronized (gameLock) {
			requireMoveState();
			move = moveInternal(start, end);
		}
		
		processEvent(new MoveEvent(GameController.class, move));
		nextPhase();
	}

	/**
	 * Pass all remaining moves.
	 */
	public static void pass() {
		synchronized (gameLock) {
			requireMoveState();
			passInternal();
			
			phase = Phase.ROLL;
		}
		
		processEvent(new PassEvent(GameController.class, currentTurn));
		
		tryTruncate();
		GameController.publishTurn(currentTurn);
		
		if (game.getTerminationCondition().isGameTerminated()) {
			phase = Phase.REPLAY;
			recordHighscore(game);
			
			processEvent(new GameOverEvent(GameController.class));
			return;
		}
		
		final List<Player> players = game.getPlayers();
		currentPlayer = players
				.get((currentPlayer.getTurnOrderPosition() + 1) % 4);
		currentTurn = new Turn(currentPlayer, null, null);
		currentPlayer.addTurn(currentTurn);
		game.addHistoryTurn(currentTurn);

		// new turn has started, process event
		processEvent(new TurnRequestEvent(GameController.class, currentTurn));
	}

	/**
	 * Processes the BoardResetEvent and notifies all event listeners.
	 * 
	 * @param event
	 *            the event object.
	 */
	public static void processEvent(final BoardResetEvent event) {
		if (!suppressEvents) {
			spawnThread( new Runnable() {
					@Override
					public void run () {
						synchronized ( eventLock ) {
							for (final BoardResetEventListener eventListener : boardResetEventListeners
									.getListeners()) {
								eventListener.onBoardResetEvent(event);
							}
						}
					}
				} );
		}
	}
	
	/**
	 * Processes the GameOverEvent and notifies all event listeners.
	 * 
	 * @param event
	 *            the event object.
	 */
	public static void processEvent(final GameOverEvent event) {
		if (!suppressEvents) {
			spawnThread( new Runnable() {
					@Override
					public void run () {
						synchronized ( eventLock ) {
							for (final GameOverEventListener eventListener : gameOverEventListeners
									.getListeners()) {
								eventListener.onGameOverEvent(event);
							}
						}
					}
				} );
		}
	}

	/**
	 * Processes the MoveEvent and notifies all event listeners.
	 * 
	 * @param event
	 *            the event object.
	 */
	public static void processEvent(final MoveEvent event) {
		if (!suppressEvents) {
			spawnThread( new Runnable() {
				@Override
				public void run () {
					synchronized ( eventLock ) {
						for (final MoveEventListener eventListener : moveEventListeners
								.getListeners()) {
							eventListener.onMoveEvent(event);
						}
					}
				}
			} );
		}
	}

	/**
	 * Processes the MoveRequestEvent and notifies all event listeners.
	 * 
	 * @param event
	 *            the event object.
	 */
	public static void processEvent(final MoveRequestEvent event) {
		if (!suppressEvents) {
			spawnThread( new Runnable() {
				@Override
				public void run () {
					synchronized ( eventLock ) {
						for (final MoveRequestEventListener eventListener : moveRequestEventListeners
								.getListeners()) {
							eventListener.onMoveRequestEvent(event);
						}
					}
				}
			} );
		}
	}

	/**
	 * Processes the PassEvent and notifies all event listeners.
	 * 
	 * @param event
	 *            the event object.
	 */
	public static void processEvent(final PassEvent event) {
		if (!suppressEvents) {
			spawnThread( new Runnable() {
				@Override
				public void run () {
					synchronized ( eventLock ) {
						for (final PassEventListener eventListener : passEventListeners
								.getListeners()) {
							eventListener.onPassEvent(event);
						}
					}
				}
			} );
		}
	}

	/**
	 * Processes the TurnRequestEvent and notifies all event listeners.
	 * 
	 * @param event
	 *            the event object.
	 */
	public static void processEvent(final TurnRequestEvent event) {
		if (!suppressEvents) {
			spawnThread( new Runnable() {
				@Override
				public void run () {
					synchronized ( eventLock ) {
						for (final TurnRequestEventListener eventListener : turnRequestEventListeners
								.getListeners()) {
							eventListener.onTurnRequestEvent(event);
						}
					}
				}
			} );
		}
	}

	/**
	 * Undo the effects of a call to {@link #undo()}.
	 */
	public static void redo() {
		synchronized (gameLock) {
			requireUndoAllowed();
			redoInternal();
		}

		processEvent(new BoardResetEvent(GameController.class, game));
	}

	/**
	 * Removes the BoardResetEvent listener.
	 * 
	 * @param eventListener
	 *            event listener object.
	 */
	public static void removeBoardResetEventListener(
			final BoardResetEventListener eventListener) {
		boardResetEventListeners.removeListener(eventListener);
	}
	
	/**
	 * Removes the GameOverEvent listener.
	 * 
	 * @param eventListener
	 *            event listener object.
	 */
	public static void removeGameOverEventListener(
			final GameOverEventListener eventListener) {
		gameOverEventListeners.removeListener(eventListener);
	}

	/**
	 * Removes the MoveEvent listener.
	 * 
	 * @param eventListener
	 *            event listener object.
	 */
	public static void removeMoveEventListener(
			final MoveEventListener eventListener) {
		moveEventListeners.removeListener(eventListener);
	}

	/**
	 * Removes the MoveRequestEvent listener.
	 * 
	 * @param eventListener
	 *            event listener object.
	 */
	public static void removeMoveRequestEventListener(
			final MoveRequestEventListener eventListener) {
		moveRequestEventListeners.removeListener(eventListener);
	}

	/**
	 * Removes the MoveEvent listener.
	 * 
	 * @param eventListener
	 *            event listener object.
	 */
	public static void removePassEventListener(
			final PassEventListener eventListener) {
		passEventListeners.removeListener(eventListener);
	}

	/**
	 * Removes the TurnRequestEvent listener.
	 * 
	 * @param eventListener
	 *            event listener object.
	 */
	public static void removeTurnRequestEventListener(
			final TurnRequestEventListener eventListener) {
		turnRequestEventListeners.removeListener(eventListener);
	}

	/**
	 * Create a random dice roll result.
	 */
	public static void roll() {
		synchronized (gameLock) {
			requireRollState();
			
			currentTurn.addUnassignedRoll((byte) (random.nextInt(6) + 1));
			currentTurn.addUnassignedRoll((byte) (random.nextInt(6) + 1));
		}
		
		nextPhase();
	}

	/**
	 * Make this game a replay-only game.
	 */
	public static void setReplayGame() {
		synchronized (gameLock) {
			phase = Phase.REPLAY;
		}
	}

	/**
	 * Undo a complete turn. This denotes a turn from the player's perspective,
	 * i.e. this method jumps to the beginning (after the dice roll) of her
	 * previous turn. If the player has made her first move, the game will
	 * revert to the beginning of the current move.
	 */
	public static void undo() {
		synchronized (gameLock) {
			requireUndoAllowed();

			undoInternal();
		}
		
		processEvent(new BoardResetEvent(GameController.class, game));
	}

	/**
	 * Trigger a boat triumph as the current player.
	 * 
	 * @param move
	 *            the move to annotate with the triumph.
	 */
	private static void boatTriumph(final PieceMove move) {
		for (final Player player : game.getPlayers()) {
			if (!player.equals(currentPlayer)) {
				for (final Piece piece : new ArrayList<Piece>(
						player.getPieces())) {
					if (piece instanceof Boat) {
						capturePiece(piece, move);
						break;
					}
				}
			}
		}
	}

	/**
	 * Capture a piece as the current player.
	 * 
	 * @param piece
	 *            the piece that was captured.
	 * @param move
	 *            the move to annotate with the capture.
	 */
	private static void capturePiece(final Piece piece, final PieceMove move) {
		piece.getSquare().setPiece(null);

		currentPlayer.addCapturedPiece(piece);
		move.addCapturedPiece(piece);

		final byte score = piece.getScore();
		currentPlayer.alterScore(score);
		move.alterScore(score);

		if (piece instanceof King) {
			for (final Piece ownedPiece : currentPlayer.getPieces()) {
				if (ownedPiece instanceof King) {
					currentPlayer.addAssassinatedKing(piece.getOwner()
							.getTurnOrderPosition());
					if (currentPlayer.getAssassinationChecklist().containsAll(
							asList((byte) 0, (byte) 1, (byte) 2, (byte) 3))) {
						currentPlayer.alterScore(ALL_KING_ASSASSINATION_SCORE);
						move.alterScore(ALL_KING_ASSASSINATION_SCORE);
						currentPlayer.clearAssassinatedKings();
					}
					break;
				}
			}
		}

		piece.getOwner().removePiece(piece);
		
		// try promoting pawns
		List<Pawn> pawns = new LinkedList<Pawn>();
		for ( final Piece remainingPiece : piece.getOwner().getPieces() ) {
			if ( remainingPiece instanceof Pawn ) {
				pawns.add( (Pawn) remainingPiece ); 
			}
		}
		for ( final Pawn pawn : pawns ) {
			tryPromotePawn( pawn, move );
		}
	}

	/**
	 * Verify that a piece is not {@code null} and owned by the current player.
	 * 
	 * @param piece
	 *            the piece to check.
	 */
	private static void checkPieceNotNullAndOwned(final Piece piece) {
		if (piece == null) {
			throw new NoPieceException();
		}
		if (!piece.getOwner().equals(currentPlayer)) {
			throw new NoPieceOwnershipException();
		}
	}

	/**
	 * Perform all steps (events, recording etc.) necessary to end the
	 * {@link Phase#MOVE1} phase.
	 */
	private static void endMove1Phase() {
		phase = Phase.MOVE2;

		tryTruncate();

		final Move move = currentTurn.getFirstMove();
		if (move instanceof PieceMove) {
			((PieceMove) move).getMovedPiece().addMove((PieceMove) move);
		}

		processEvent(new MoveRequestEvent(GameController.class, currentTurn));
	}

	/**
	 * Perform all steps (events, recording etc.) necessary to end the
	 * {@link Phase#MOVE2} phase.
	 */
	private static void endMove2Phase() {
		phase = Phase.ROLL;

		final Move move = currentTurn.getSecondMove();
		if (move instanceof PieceMove) {
			((PieceMove) move).getMovedPiece().addMove((PieceMove) move);
		}

		GameController.publishTurn(currentTurn);

		if (game.getTerminationCondition().isGameTerminated()) {
			phase = Phase.REPLAY;
			recordHighscore(game);
			
			processEvent(new GameOverEvent(GameController.class));
			return;
		}

		final List<Player> players = game.getPlayers();
		currentPlayer = players
				.get((currentPlayer.getTurnOrderPosition() + 1) % 4);
		currentTurn = new Turn(currentPlayer, null, null);
		currentPlayer.addTurn(currentTurn);
		game.addHistoryTurn(currentTurn);

		// new turn has started, process event
		processEvent(new TurnRequestEvent(GameController.class, currentTurn));
	}

	/**
	 * Perform all steps (events, recording etc.) necessary to end the
	 * {@link Phase#ROLL} phase.
	 */
	private static void endRollPhase() {
		phase = Phase.MOVE1;

		// first move is required, process event
		processEvent(new MoveRequestEvent(GameController.class, currentTurn));
	}

	/**
	 * Get a roll from the {@link Turn#getUnassignedRolls()} that allows the
	 * chosen piece to move. The roll is unavailable afterwards.
	 * 
	 * @param piece
	 *            the moving piece,must not be {@code null}.
	 * @return the compatible roll.
	 */
	private static byte getRollCompatibleWithPiece(final Piece piece) {
		final EquivalenceClass equivalenceClass = DiceRoll
				.equivalenceClassFromPiece(piece);

		for (final byte roll : currentTurn.getUnassignedRolls()) {
			if (equivalenceClassFromRoll(roll) == equivalenceClass) {
				currentTurn.removeUnassignedRoll(roll);
				return roll;
			}
		}
		throw new IllegalMoveException();
	}

	/**
	 * Initialize the game board for a single player.
	 * 
	 * @param board
	 *            the board to initialize, must not be {@code null} and have 8x8
	 *            squares.
	 * @param player
	 *            the player for whom to initialize, must not be {@code null}.
	 * @param boatPos
	 *            the position of the player's boat, must not be {@code null}.
	 * @param forward
	 *            the direction in which the player's pawns move, must not be
	 *            {@code null}.
	 * @param right
	 *            the direction from the player's boat to her king, must not be
	 *            {@code null}.
	 */
	private static void initBoardPlayer(final Square[][] board,
			final Player player, final Position boatPos,
			final Movement forward, final Movement right) {
		final Square boatSquare = board[boatPos.getX()][boatPos.getY()];
		final Boat boat = new Boat(player, boatSquare);
		boatSquare.setPiece(boat);
		player.addPiece(boat);

		final Position horsePos = CloneUtil.clone(boatPos);
		horsePos.shift(right);
		final Square horseSquare = board[horsePos.getX()][horsePos.getY()];
		final Horse horse = new Horse(player, horseSquare);
		horseSquare.setPiece(horse);
		player.addPiece(horse);

		final Position elephantPos = CloneUtil.clone(horsePos);
		elephantPos.shift(right);
		final Square elephantSquare = board[elephantPos.getX()][elephantPos
				.getY()];
		final Elephant elephant = new Elephant(player, elephantSquare);
		elephantSquare.setPiece(elephant);
		player.addPiece(elephant);

		final Position kingPos = CloneUtil.clone(elephantPos);
		kingPos.shift(right);
		final Square kingSquare = board[kingPos.getX()][kingPos.getY()];
		final King king = new King(player, kingSquare);
		kingSquare.setPiece(king);
		player.addPiece(king);

		final Position pawnPos = CloneUtil.clone(boatPos);
		pawnPos.shift(forward);
		for (int i = 0; i < 4; i++) {
			final Square pawnSquare = board[pawnPos.getX()][pawnPos.getY()];
			final Pawn pawn = new Pawn(player, pawnSquare);
			pawnSquare.setPiece(pawn);
			player.addPiece(pawn);
			pawnPos.shift(right);
		}
	}

	/**
	 * Move a piece over the board. This does not trigger captures etc.
	 * 
	 * @param piece
	 *            the moving piece.
	 * @param target
	 *            the target position.
	 * @param move
	 *            the move to annotate with the movement.
	 */
	private static void movePiece(final Piece piece, final Position target,
			final Move move) {
		piece.getSquare().setPiece(null);
		final Square targetSquare = game.getSquare(target);
		piece.setSquare(targetSquare);
		targetSquare.setPiece(piece);
	}

	/**
	 * Finish a phase, begin the next one. Starts the next {@link Turn},
	 * triggers events etc.
	 */
	private static void nextPhase() {
		switch (phase) {
		case ROLL:
			endRollPhase();
			break;
		case MOVE1:
			endMove1Phase();
			break;
		case MOVE2:
			endMove2Phase();
			break;
		}
	}

	/**
	 * Publish a turn via eMail, notifying all {@link #EMAIL} players about the
	 * turn.
	 * 
	 * @param turn
	 *            the completed turn to send.
	 */
	private static void publishTurn(final Turn turn) {
		try {
			final SimpleEmail email = new SimpleEmail();

			email.setFrom(getSenderMailAddress());
			email.setSubject("[CHATURAJI] turn");

			for (final Player player : turn.getPlayer().getGame().getPlayers()) {
				if (player.getType() == EMAIL) {
					email.addTo(player.getName());
				}
			}

			if (email.getToAddresses().isEmpty()) {
				return;
			}

			email.setHostName(getMailServer());
			email.setAuthentication(getMailUsername(), getMailPassword());

			email.setContent(serializeTurn(turn), "text/plain");

			email.send();
		} catch (final EmailException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Implement the workings of {@link #redo()} without locking (for internal
	 * calls).
	 */
	private static void redoInternal() {
		if (truncate == null) {
			throw new BadStateException();
		}

		final List<Turn> history = game.getHistory();
		final int distance = history.size() - 1 - truncate;
		if (distance == 0) {
			restoreTurn(history.get(++truncate));
			currentTurn = history.get(truncate);
			truncate = null;
		} else if (phase == Phase.REPLAY) {
			restoreTurn(history.get(++truncate));
			currentTurn = history.get(truncate);
		} else {
			if (distance < 4) {
				throw new BadStateException();
			}
			Turn pointedTurn = null;
			int stop = truncate + 4;
			for (; truncate <= stop; truncate++) {
				pointedTurn = history.get(truncate);
				restoreTurn(pointedTurn);
			}
			currentTurn = pointedTurn;
		}
	}

	/**
	 * Require that the controller is in {@link Phase#MOVE1} or
	 * {@link Phase#MOVE2}.
	 */
	private static void requireMoveState() {
		if ((phase != Phase.MOVE1) && (phase != Phase.MOVE2)) {
			throw new BadStateException();
		}
	}

	/**
	 * Require that the controller is in {@link Phase#ROLL}.
	 */
	private static void requireRollState() {
		if (phase != Phase.ROLL) {
			throw new BadStateException();
		}
	}

	/**
	 * Require that the game allows {@link #undo()}. Thus, the controller either
	 * must be in {@link Phase#REPLAY} or have only one human player
	 */
	private static void requireUndoAllowed() {
		if (phase != Phase.ROLL) {
			boolean humanFound = false;
			for (final Player player : game.getPlayers()) {
				if ((player.getType() == HUMAN) || (player.getType() == EMAIL)) {
					if (humanFound) {
						throw new BadStateException();
					} else {
						humanFound = true;
					}
				}
			}
		}
	}

	/**
	 * Restore the effects of a move. This undoes the effect of
	 * {@link #revertMove(Turn, Move)}. All previous turns and moves must have
	 * been restored.
	 * 
	 * @param turn
	 *            the turn containing the move to restore.
	 * @param move
	 *            the move to restore.
	 */
	private static void restoreMove(final Turn turn, final Move move) {
		if ((move == null) || !(move instanceof PieceMove)) {
			return;
		}

		final PieceMove pieceMove = (PieceMove) move;
		final Player player = turn.getPlayer();

		player.alterScore(pieceMove.getScore());
		player.addCapturedPieces(pieceMove.getCapturedPieces());

		final Piece piece = pieceMove.getMovedPiece();

		for (final Piece capturedPiece : pieceMove.getCapturedPieces()) {
			capturedPiece.getOwner().removePiece(capturedPiece);
			capturedPiece.getSquare().setPiece(null);
			player.alterScore(capturedPiece.getScore());
		}

		final Square endSquare = game.getSquare(pieceMove.getEndPosition());
		piece.setSquare(endSquare);
		endSquare.setPiece(piece);

		game.getSquare(pieceMove.getStartPosition()).setPiece(null);

		for (final Pawn promotionSourcePawn : pieceMove
				.getPromotionSourcePawns()) {
			promotionSourcePawn.getOwner().removePiece(promotionSourcePawn);
		}

		for (final Piece promotedPawn : pieceMove.getPromotedPawns()) {
			promotedPawn.getOwner().addPiece(promotedPawn);
			promotedPawn.getSquare().setPiece(promotedPawn);
		}

		kingAssassinationSearch: for (final Piece capturedPiece : pieceMove
				.getCapturedPieces()) {
			if (capturedPiece instanceof King) {
				for (final Piece ownedPiece : currentPlayer.getPieces()) {
					if (ownedPiece instanceof King) {
						if (player.addAssassinatedKing(capturedPiece.getOwner()
								.getTurnOrderPosition())) {
							if (player.getAssassinationChecklist().containsAll(
									asList((byte) 0, (byte) 1, (byte) 2,
											(byte) 3))) {
								player.clearAssassinatedKings();
								player.alterScore(ALL_KING_ASSASSINATION_SCORE);
							}
						}
						break kingAssassinationSearch;
					}
				}
			}
		}
	}

	/**
	 * Restore the effects of a turn. This undoes the effects of
	 * {@link #revertTurn(Turn)}. All previous turns must have been restored.
	 * 
	 * @param turn
	 *            the turn to restore.
	 */
	private static void restoreTurn(final Turn turn) {
		restoreMove(turn, turn.getFirstMove());
		restoreMove(turn, turn.getSecondMove());
	}

	/**
	 * Take back a move, i.e. revert all its effects except for entries in the
	 * game, player and piece histories. All moves and turns after this move
	 * MUST have been reverted.
	 * 
	 * @param turn
	 *            the turn containing the move to revert.
	 * @param move
	 *            the move to revert.
	 */
	private static void revertMove(final Turn turn, final Move move) {
		if ((move == null) || !(move instanceof PieceMove)) {
			return;
		}

		final PieceMove pieceMove = (PieceMove) move;
		final Player player = turn.getPlayer();

		player.alterScore(-pieceMove.getScore());
		player.removeCapturedPieces(pieceMove.getCapturedPieces());

		final Piece piece = pieceMove.getMovedPiece();

		final Square startSquare = game.getSquare(pieceMove.getStartPosition());
		piece.setSquare(startSquare);
		startSquare.setPiece(piece);
		game.getSquare(pieceMove.getEndPosition()).setPiece(null);

		for (final Piece promotedPawn : pieceMove.getPromotedPawns()) {
			promotedPawn.getOwner().removePiece(promotedPawn);
		}

		for (final Pawn promotionSourcePawn : pieceMove
				.getPromotionSourcePawns()) {
			promotionSourcePawn.getOwner().addPiece(promotionSourcePawn);
			promotionSourcePawn.getSquare().setPiece(promotionSourcePawn);
		}

		for (final Piece capturedPiece : pieceMove.getCapturedPieces()) {
			capturedPiece.getOwner().addPiece(capturedPiece);
			capturedPiece.getSquare().setPiece(capturedPiece);
			player.alterScore(capturedPiece.getScore());
		}

		kingAssassinationSearch: for (final Piece capturedPiece : pieceMove
				.getCapturedPieces()) {
			if (capturedPiece instanceof King) {
				for (final Piece ownedPiece : currentPlayer.getPieces()) {
					if (ownedPiece instanceof King) {
						if (!player.removeAssassinatedKing(capturedPiece
								.getOwner().getTurnOrderPosition())) {
							for (byte position = 0; position < 3; position++) {
								if (position != capturedPiece.getOwner()
										.getTurnOrderPosition()) {
									player.addAssassinatedKing(position);
								}
							}
							player.alterScore(-ALL_KING_ASSASSINATION_SCORE);
						}
						break kingAssassinationSearch;
					}
				}
			}
		}
	}

	/**
	 * Take back a turn, i.e. revert all its effects except for entries in the
	 * game, player and piece histories. All turns after this MUST have been
	 * reverted.
	 * 
	 * @param turn
	 *            the turn to revert.
	 */
	private static void revertTurn(final Turn turn) {
		revertMove(turn, turn.getSecondMove());
		revertMove(turn, turn.getFirstMove());
	}

	/**
	 * Attempt to promote a pawn. This does nothing if the pawn can not be
	 * promoted because the respective piece is still alive.
	 * 
	 * @param pawn
	 *            the pawn to promote.
	 * @param move
	 *            the move to annotate with the promotion.
	 */
	private static void tryPromotePawn(final Pawn pawn, final PieceMove move) {
		if ( !pawn.canPromote() )
			return;
		
		final Square square = pawn.getSquare();
		final Position pos = square.getPosition();
		
		byte promotionPoint;
		if ( pos.getX() == 0 || pos.getX() == 7 )
			promotionPoint = pos.getY();
		else if ( pos.getY() == 0 || pos.getY() == 7 )
			promotionPoint = pos.getX();
		else
			return;
		
		// promotion
		final Piece piece;
		final Player owner = pawn.getOwner();
		
		switch ( promotionPoint )
		{
			case 0:
			case 7: // Boat
				for ( final Piece currentPiece : owner.getPieces() ) {
					if ( currentPiece instanceof Boat )
						return;
				}
				piece = new Boat( owner, square );
				break;
			
			case 1:
			case 6: // Horse
				for ( final Piece currentPiece : owner.getPieces() ) {
					if ( currentPiece instanceof Horse )
						return;
				}
				piece = new Horse( owner, square );
				break;
				
			case 2:
			case 5: // Elephant
				for ( final Piece currentPiece : owner.getPieces() ) {
					if ( currentPiece instanceof Elephant )
						return;
				}
				piece = new Elephant( owner, square );
				break;
				
			case 3:
			case 4: // King
				for ( final Piece currentPiece : owner.getPieces() ) {
					if ( currentPiece instanceof King )
						return;
				}
				piece = new King( owner, square );
				break;
			
			default:
				throw new IllegalStateException( "illegal promotion position" );
		}
		
		// replace piece
		move.addPromotedPawn( piece );
		move.addPromotionSourcePawn( pawn );
		owner.removePiece( pawn );
		owner.addPiece( piece );
		square.setPiece( piece );
	}

	/**
	 * Truncate, if necessary, the history after turns have been reverted. If
	 * there were no reversion, this does nothing.
	 */
	private static void tryTruncate() {
		if (truncate != null) {
			final List<Turn> history = game.getHistory();
			final List<Turn> turnsToDelete = history.subList(truncate,
					history.size());
			final Map<Player, List<Turn>> playerTurnsToDelete = new HashMap<Player, List<Turn>>();
			final Map<Piece, List<PieceMove>> pieceMovesToDelete = new HashMap<Piece, List<PieceMove>>();
			for (final Turn turnToDelete : turnsToDelete) {
				addToListMap(playerTurnsToDelete, turnToDelete.getPlayer(),
						turnToDelete);
				Move moveToDelete = turnToDelete.getFirstMove();
				if (moveToDelete instanceof PieceMove) {
					addToListMap(pieceMovesToDelete,
							((PieceMove) moveToDelete).getMovedPiece(),
							(PieceMove) moveToDelete);
				}
				moveToDelete = turnToDelete.getSecondMove();
				if (moveToDelete instanceof PieceMove) {
					addToListMap(pieceMovesToDelete,
							((PieceMove) moveToDelete).getMovedPiece(),
							(PieceMove) moveToDelete);
				}
			}

			game.removeHistoryTurns(turnsToDelete);
			for (final Entry<Player, List<Turn>> entry : playerTurnsToDelete
					.entrySet()) {
				entry.getKey().removeTurns(entry.getValue());
			}
			for (final Entry<Piece, List<PieceMove>> entry : pieceMovesToDelete
					.entrySet()) {
				entry.getKey().removeMoves(entry.getValue());
			}

			truncate = null;
		}
	}

	/**
	 * Implement {@link #undo()} without locking (for internal calls).
	 */
	private static void undoInternal() {
		final List<Turn> history = game.getHistory();
		if (truncate == null) {
			truncate = history.size();
		}
		switch (phase) {
		case ROLL:
		case MOVE1:
			if (truncate < 4) {
				throw new BadStateException();
			}
			Turn pointedTurn = null;
			int stop = truncate - 5;
			for (truncate--; truncate >= stop; truncate--) {
				pointedTurn = history.get(truncate);
				revertTurn(pointedTurn);
			}
			currentTurn = new Turn(currentPlayer, null, null);
			currentTurn.addUnassignedRolls(pointedTurn.getUnassignedRolls());
			phase = Phase.MOVE1;
			break;
		case MOVE2:
			revertTurn(history.get(--truncate));
			final Collection<Byte> rolls = currentTurn.getUnassignedRolls();
			currentTurn = new Turn(currentPlayer, null, null);
			currentTurn.addUnassignedRolls(rolls);
			phase = Phase.MOVE1;
			break;
		case REPLAY:
			if (truncate < 2) {
				throw new BadStateException();
			}
			revertTurn(history.get(--truncate));
			currentTurn = history.get(truncate - 1);
			break;
		}
		currentPlayer.taint();
	}

	/**
	 * Write a completed move to the correct move field in the current turn.
	 * 
	 * @param move
	 *            the move to write.
	 */
	private static void writeMove(final Move move) {
		switch (phase) {
		case MOVE1:
			currentTurn.setFirstMove(move);
			break;
		case MOVE2:
			currentTurn.setSecondMove(move);
			break;
		}
	}

	/**
	 * Set the event suppression status.
	 * 
	 * @param suppress
	 *            {@code true} to suppress events, {@code false} to enable them.
	 */
	static void controlEventSuppression(final boolean suppress) {
		suppressEvents = suppress;
	}

	/**
	 * Initialize the board according to the chaturaji rules.
	 */
	static void initBoard() {
		synchronized (gameLock) {
			final Square[][] board = game.getBoard();
			final List<Player> players = game.getPlayers();

			initBoardPlayer(board, players.get(0), new Position((byte) 0,
					(byte) 0), new Movement((byte) 1, (byte) 0), new Movement(
					(byte) 0, (byte) 1));
			initBoardPlayer(board, players.get(1), new Position((byte) 7,
					(byte) 0), new Movement((byte) 0, (byte) 1), new Movement(
					(byte) -1, (byte) 0));
			initBoardPlayer(board, players.get(2), new Position((byte) 7,
					(byte) 7), new Movement((byte) -1, (byte) 0), new Movement(
					(byte) 0, (byte) -1));
			initBoardPlayer(board, players.get(3), new Position((byte) 0,
					(byte) 7), new Movement((byte) 0, (byte) -1), new Movement(
					(byte) 1, (byte) 0));
		}
	}

	/**
	 * The internal implementation of {@link #move(Position, Position)}
	 * 
	 * @param start
	 *            the start position.
	 * @param end
	 *            the end position.
	 * @return the resulting move.
	 */
	static PieceMove moveInternal(final Position start, final Position end) {
		PieceMove move;

		final Piece piece = game.getBoard()[start.getX()][start.getY()]
				.getPiece();
		checkPieceNotNullAndOwned(piece);

		final byte roll = getRollCompatibleWithPiece(piece);

		final Actions actions = piece.getPossibleActions();

		if (actions.getPossibleMoves().contains(end)) {
			move = new PieceMove(currentTurn, roll, start, end, piece,
					(byte) 0, null, null, null);
			movePiece(piece, end, move);

		} else if (actions.getPossibleCaptures().contains(end)) {
			move = new PieceMove(currentTurn, roll, start, end, piece,
					(byte) 0, null, null, null);
			capturePiece(game.getSquare(end).getPiece(), move);
			movePiece(piece, end, move);

		} else {
			// readd previously removed roll
			currentTurn.addUnassignedRoll(roll);

			throw new IllegalMoveException();
		}

		if (end.equals(actions.getPossibleBoatTriumph())) {
			boatTriumph(move);

		} else if (actions.getPossiblePawnPromotions().contains(end)) {
			tryPromotePawn((Pawn) piece, move);

		}

		writeMove(move);

		return move;
	}

	/**
	 * Implement the logic of move passing.
	 * 
	 * @return the passes generated.
	 */
	static Collection<Pass> passInternal() {
		final Collection<Pass> ret = new LinkedList<Pass>();
		for (final byte roll : currentTurn.getUnassignedRolls()) {
			final Pass pass = new Pass(roll);
			writeMove(pass);
			ret.add(pass);
		}
		return ret;
	}

	/**
	 * Set a new game and move to the right state.
	 * 
	 * @param game
	 *            the game to set, must not be {@code null}.
	 */
	static void setGame(final Game game) {
		GameController.game = game;
		
		spawnThread( new Runnable() {
			@Override
			public void run () {
				final List<Turn> history = game.getHistory();
				if (history.isEmpty()) {
					initBoard();
					processEvent(new BoardResetEvent(GameController.class, game));
		
					currentPlayer = game.getPlayers().get(0);
					currentTurn = new Turn(currentPlayer, null, null);
		
					// add turn
					currentPlayer.addTurn(currentTurn);
					game.addHistoryTurn(currentTurn);
		
					phase = Phase.ROLL;
					processEvent(new TurnRequestEvent(GameController.class, currentTurn));
				} else {
					processEvent(new BoardResetEvent(GameController.class, game));
		
					final Turn latestTurn = history.get(history.size() - 1);
					if (latestTurn.getFirstMove() == null) {
						currentPlayer = game.getPlayers().get((history.size() - 1) % 4);
						currentTurn = latestTurn;
		
						phase = Phase.MOVE1;
						processEvent(new MoveRequestEvent(GameController.class,
								currentTurn));
					} else if (latestTurn.getSecondMove() == null) {
						currentPlayer = game.getPlayers().get((history.size() - 1) % 4);
						currentTurn = latestTurn;
		
						phase = Phase.MOVE2;
						processEvent(new MoveRequestEvent(GameController.class,
								currentTurn));
					} else {
						currentPlayer = game.getPlayers().get(history.size() % 4);
						currentTurn = new Turn(currentPlayer, null, null);
		
						// add turn
						currentPlayer.addTurn(currentTurn);
		
						phase = Phase.ROLL;
						processEvent(new TurnRequestEvent(GameController.class,
								currentTurn));
					}
				}
			}
		} );
	}

	/**
	 * Just set a game.
	 * 
	 * @param game
	 *            the new game, must not be {@code null}.
	 */
	static void setPreliminaryGame(final Game game) {
		synchronized (gameLock) {
			GameController.game = game;
			currentPlayer = game.getPlayers().get(0);
			currentTurn = new Turn(currentPlayer, null, null);
			currentPlayer.addTurn(currentTurn);
			phase = Phase.ROLL;
		}
	}

	/**
	 * Always throw a {@link UnsupportedOperationException} to prevent
	 * instantiation.
	 */
	private GameController() {
		throw new UnsupportedOperationException("can't instantiate singleton");
	}
	
	/**
	 * Returns a list of all serialized log entries.
	 * 
	 * @return log list.
	 */
	public static List<String> getLog ()
	{
		ArrayList<String> log = new ArrayList<String>( game.getHistory().size() * 2 );
		
		for ( Turn turn : game.getHistory() )
		{
			if ( turn.getFirstMove() != null )
				log.add( SystemController.serializeMove( turn, turn.getFirstMove() ) );
			
			if ( turn.getSecondMove() != null )
				log.add( SystemController.serializeMove( turn, turn.getSecondMove() ) );
		}
		
		return log;
	}
}
